//////////////////////////////////////////////////////////////
//	Table (De)Serialiser Module
//	Turns a table in to a file with a header
//	Module and common string pooling by: TAD2020
//	(De)Serialise functions by the awesome DEADBEEF
//////////////////////////////////////////////////////////////

Serialise = {}

// Version

Serialise.Version = Version

// String pooling (TAD2020)

function Serialise.PoolString(String, Table, Not)
	if (Not) or (string.len(String) < 4) then
		return string.format('S:%q', String)
	end
	
	// IDX is nil to start with
	
	local IDX = nil
	
	// Does it exist
	
	if (Table.StringIndx[String]) then
		Table.Saved = Table.Saved + 1
		
		IDX = string.format('Z:%q', tostring(Table.StringIndx[String]))
	else
		Table.LastIndx = Table.LastIndx + 1
		Table.StringIndx[String] = Table.LastIndx
		Table.Strings[Table.LastIndx] = String
		
		IDX = string.format('Z:%q', tostring(Table.LastIndx))
	end
	
	return IDX
end

// Serialise a chunk

function Serialise.SerialiseChunk(Chunk, Tables, Table, Not)
	// Get the data type
	
	local Type = type(Chunk)

	// See what data type this chunk represents and return the proper value
	
	if		Type == "number"  then return 'N:'..Chunk
	elseif	Type == "string"  then return Serialise.PoolString(Chunk, Table)
	elseif	Type == "boolean" then return 'B:'..tostring(Chunk):sub(1,1)
	elseif	Type == "Entity"  then if Chunk == GetWorldEntity() then return 'E:W' elseif chunk == NULL then return 'E:N' else return 'E:'..Chunk:EntIndex()  end
	elseif	Type == "Vector"  then return string.format("V:%g,%g,%g", Chunk.x, Chunk.y, Chunk.z)
	elseif	Type == "Angle"   then return string.format("A:%g,%g,%g", Chunk.pitch, Chunk.yaw, Chunk.roll)
	elseif	Type == "Player"  then return 'P:'..Chunk:UniqueID()
	elseif	Type == "table"	 then
		local ID = tostring(Chunk):sub(8)
		
		// Blah blah check
		
		if (!Tables[ID]) then Tables[ID] = true Tables[ID] = Serialise.SerialiseTableKeyValues(Chunk, Tables, Table, Not) end
		
		return 'T:'..ID
	end
end

// Deserialise a chunk

function Serialise.DeserialiseChunk(Chunk, Tables, Table)
	// Get the data type and value
	
	local Type, Val = Chunk:match("(.):(.+)")
	
	// See what data type this chunk represents and return the proper value
	
	if		Type == "N" then return tonumber(Val)
	elseif	Type == "S" then return Val:sub(2, -2)
	elseif	Type == "Z" then return Table[Val:sub(2, -2)]
	elseif	Type == "B" then return Val == "t"
	elseif	Type == "E" then if Val == "W" then return GetWorldEntity() elseif Val == "N" then return NULL else return Entity(Val)  end
	elseif	Type == "V" then
		local A, B, C = Val:match("(.-),(.-),(.+)")
		
		return Vector(tonumber(A), tonumber(B), tonumber(C))
	elseif	Type == "A" then
		local A, B, C = Val:match("(.-),(.-),(.+)")
		
		return Angle(tonumber(A), tonumber(B), tonumber(C))
	elseif	Type == "P" then return player.GetByUniqueID(Val)
	elseif	Type == "T" then 
		local T = {}
		
		if (!Tables[Val]) then Tables[Val] = {} end
		
		table.insert(Tables[Val], T)
		
		return T
	end
end

// Serialise table to key values

function Serialise.SerialiseTableKeyValues(TBL, Tables, Table, Not)
	local Temp = {}
	
	local Keys = !table.IsSequential(TBL)

	for K, V in pairs(TBL) do
		local String = Serialise.SerialiseChunk(V, Tables, Table, Not)
		
		if (Keys) and (String) then String = Serialise.SerialiseChunk(K, Tables, Table, Not).."="..String end
		
		table.insert(Temp, String)
	end

	return Temp
end

// Serialise with headers

function Serialise.SerialiseWithHeaders(Header, Extra, T, Not)
	local Tables = {}
	local String = ""
	
	local Table = {}
	
	Table.Strings = {}
	Table.StringIndx = {}
	Table.LastIndx = 0
	Table.Saved = 0
	
	// Head and HeadID
	
	local HeadID = tostring(T):sub(8)
	local Head	 = Serialise.SerialiseTableKeyValues(T, Tables, Table, Not)

	// Distinguish the main table from nested tables
	
	Tables[HeadID] = nil
	Tables["H"..HeadID] = Head
	
	for K, V in pairs(Tables) do
		// Concat key values for each table
		
		String = String..K.."{"..table.concat(V, ";")..";}"
	end
	
	// IDX
	
	for IDX, CSTR in pairs(Table.Strings) do
		String = table.concat({String, "\n", IDX, ":", CSTR})
	end
	
	String = String.."\nSaved:"..Table.Saved
	
	// Concat
	
	return table.concat(
		{
			"[Information]",
			table.concat(Header, "\n"),
			"[More Information]",
			table.concat(Extra, "\n"),
			"[Save]",
			String
		}, "\n"
	)
end

// Pass this function the output from the previous one

function Serialise.DeserialiseWithHeaders(In)
	local Tables	= {}
	local Subtables	= {}
	local Head		= nil
	
	// Header, extra and data
	
	local Header, Extra, Data = In:match("%[Information%]\n(.+)\n%[More Information%]\n(.+)\n%[Save%]\n(.+)")
	
	// Check
	
	if (!Header) or (!Extra) or (!Data) then
		return false
	end
	
	// Header
	
	local Table = {}
	
	for K, V in pairs(string.Explode("\n", Header)) do
		local Key, CSTR = V:match("(.-):(.+)")
		
		Table[Key] = CSTR
	end
	
	// Extra

	local Extras = {}
	
	for K, V in pairs(string.Explode("\n", Extra)) do
		local Key, CSTR = V:match("(.-):(.+)")
		
		Extras[Key] = CSTR
	end
	
	// Blocks
	
	local Blocks = string.Explode("\n", Data)
	local Block = Blocks[1]
	
	local STR = {}
	
	for I = 2, (#Blocks - 1) do
		local Key, CSTR = Blocks[I]:match("(.-):(.+)")
		
		STR[Key] = CSTR
	end
	
	// Chunks
	
	for ID, Chunk in Block:gmatch('(%w+){(.-)}') do // Get each table chunk
		// Check if this table is the trunk
		
		if (ID:sub(1, 1) == "H") then ID = ID:sub(2) Head = ID end
		
		Tables[ID] = {}
		
		for KV in Chunk:gmatch('(.-);') do // Split each table block into k/v pairs
			local K, V = KV:match('(.-)=(.+)') // Deserialise each k/v pair and add to the new table
			
			// If the table keys are sequential digits they won't be included in the string
			
			if (!K) then
				V = Serialise.DeserialiseChunk(KV, Subtables, STR)
				
				table.insert(Tables[ID], V)
			else
				K = Serialise.DeserialiseChunk(K, Subtables, STR)
				V = Serialise.DeserialiseChunk(V, Subtables, STR)
				
				Tables[ID][K] = V
			end
		end
	end
	
	// Restore table references
	
	for ID, TBLS in pairs(Subtables) do
		for _, TBL in pairs(TBLS) do
			table.Merge(TBL, Tables[ID])
		end
	end
	
	return Table, Extras, Tables[Head]
end

// Deserialise

function Serialise.Deserialise(...)
	local Block = table.concat(arg)
	local Tables	= {}
	local Subtables	= {}
	local Head		= nil

	for ID, Chunk in Block:gmatch('(%w+){(.-)}') do
		if ID:sub(1, 1) == "H" then ID = ID:sub(2) Head = ID end
		
		Tables[ID] = {}
		
		for KV in Chunk:gmatch('(.-);') do
			local K, V = KV:match('(.-)=(.+)')
			
			if (!K) then
				V = Serialise.DeserialiseChunk(KV, Subtables)
				
				table.insert(Tables[ID], V)
			else
				K = Serialise.DeserialiseChunk(K, Subtables)
				V = Serialise.DeserialiseChunk(V, Subtables)
				
				Tables[ID][K] = V
			end
		end
	end
	
	for ID, TBLS in pairs(Subtables) do
		for _, TBL in pairs(TBLS) do
			table.Merge(TBL, Tables[ID])
		end
	end

	return Tables[Head]
end